## pak文件
## 文件格式为：
##顺序	字节数	含义
##1		9 byte	pak文件标识，0xC04AC0BA0000000000
##2		x byte	文件头块，由若干个文件头组成，每个文件头的结尾为0x00，最后一个的文件头结尾为0x80
##3		y byte	数据块，由若干个文件数据组成，文件数据的长度记录在文件头中
class_name PakFile
extends RefCounted

#region const
## 密匙异或值
const SECRET_XOR_VALUE: int = 0xF7
## pak文件标识缓冲区（十六进制：0xC04AC0BA0000000000 十进制：192 74 192 186 0 0 0 0 0）
static var PAK_FILE_SIGN_BUFFER: PackedByteArray = PackedByteArray([ 0xC0, 0x4A, 0xC0, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00 ])
## pak文件标识缓冲区（已异或 十六进制：0x37BD374DF7F7F7F7F7 十进制：55 189 55 77 247 247 247 247 247）
static var PAK_FILE_SIGN_BUFFER_XOR: PackedByteArray = PackedByteArray([ 0xC0 ^ SECRET_XOR_VALUE, 0x4A ^ SECRET_XOR_VALUE, 0xC0 ^ SECRET_XOR_VALUE, 0xBA ^ SECRET_XOR_VALUE, 0x00 ^ SECRET_XOR_VALUE, 0x00 ^ SECRET_XOR_VALUE, 0x00 ^ SECRET_XOR_VALUE, 0x00 ^ SECRET_XOR_VALUE, 0x00 ^ SECRET_XOR_VALUE ])
## pak文件标识的长度-9
static var PAK_FILE_SIGN_LENGTH: int = PAK_FILE_SIGN_BUFFER.size()
#endregion const

#region variable
## 文件头数组
var Headers: Array[PakFileHeader] = []
## pak文件路径
var _path: String = ""
## 文件内容数组
var _contents: Array[PakFileContent] = []
## 文件是否已改变
var _isChanged: bool = false
#endregion variable

#region Init&Common
## 构造函数
## 参数：
## headers-文件头数组
## contents-文件内容数组
## 备注：如果headers与contents数组的长度不一致，仅使用更少的元素个数用于初始化。
func _init(headers: Array[PakFileHeader] = [], contents: Array[PakFileContent] = [], path: String = "") -> void:
	var length: int = mini(headers.size(), contents.size())
	if length == 0:
		return
	Headers = Array(headers.slice(0, length - 1))
	_contents = Array(contents.slice(0, length - 1))
	_path = path
	_isChanged = false

## 获取文件头的数目
## 返回：文件头的数目。
func GetHeadersCount() -> int:
	return Headers.size() if Headers else 0
	
## 获取文件头的长度
## 返回：文件头的长度（字节数）。
func GetHeadersLength() -> int:
	var length: int = 0
	for header: PakFileHeader in Headers:
		length += header.GetLength()
	return length

## 获取pak文件路径
## 返回：pak文件路径
func GetPath() -> String:
	return _path

## 获取文件是否已改变
## 返回：文件是否已改变
## 备注：文件在插入、添加、修改、删除之后设置已改变标记；在保存或者另存之后，清除已改变标记。
func GetIsChanged() -> bool:
	return _isChanged
	
## 检查pak文件签名
## 参数：
## file-文件。
## 返回结果
static func CheckSign(file: FileAccess) -> ActionResult:
	if file.get_length() < PAK_FILE_SIGN_LENGTH:
		return ActionResult.new(FAILED, "文件大小不能小于{0}字节。".format([ PAK_FILE_SIGN_LENGTH ]))
	file.seek(0)
	var _sign: PackedByteArray = file.get_buffer(PAK_FILE_SIGN_LENGTH)
	if _sign != PAK_FILE_SIGN_BUFFER_XOR:
		return ActionResult.new(ERR_FILE_UNRECOGNIZED, "PAK文件标识错误：{0}".format([ sign ]))
	return ActionResult.new(OK)
	
## 对数组执行异或操作
## 参数：
## buffer-缓冲区
## start-执行异或操作的起点
## count-执行异或操作的元素数目
static func XOR(buffer: PackedByteArray, start: int = 0, count: int = 0) -> void:
	ArrayUtility.XOR(buffer, SECRET_XOR_VALUE, start, count)
#endregion Init&Common

#region Load
## 加载
## 参数：
## path-文件路径。
## 返回：加载的结果。
## 备注：结果的Data类型是PakFile，如果加载失败，Data为null。
static func Load(path: String) -> ActionResultWithData:
	# 判断文件是否存在
	if not FileAccess.file_exists(path):
		return ActionResultWithData.new(ERR_FILE_NOT_FOUND, "未找到文件“{0}”。".format([ path ]))
	# 打开文件
	var file: FileAccess = FileAccess.open(path, FileAccess.READ)
	if not file:
		var error: Error = FileAccess.get_open_error()
		return ActionResultWithData.new(error, "打开文件“{0}”失败。失败原因：{1}".format([ path, error ]))
	# 校验签名
	var checkSignResult: ActionResult = CheckSign(file)
	if not checkSignResult.Success():
		file.close()
		return ActionResultWithData.new(checkSignResult.Error, checkSignResult.Message)
	# 读取文件头
	var headers: Array[PakFileHeader] = PakFileHeader.ParseArrayFromFile(file)
	var originalPos: int = file.get_position()
	file.close()
	if not headers or headers.size() == 0:
		return ActionResultWithData.new(ERR_FILE_CORRUPT, "解析文件头错误。")
	# 生成文件内容
	var contents: Array[PakFileContent] = []
	for header: PakFileHeader in headers:
		var content: PakFileContent = PakFileContent.new()
		content.SetOriginal(originalPos, header.FileSize)
		contents.append(content)
		originalPos += header.FileSize
	# 返回
	var pakFile: PakFile = PakFile.new(headers, contents, path)
	return ActionResultWithData.new(OK, "", pakFile)
#endregion Load

#region Save
## 保存
## 返回：保存的结果。
func Save() -> ActionResult:
	return SaveAs(_path, true)

## 另存为
## 参数：
## path-另存的文件路径。
## overwrite-如果文件已存在，是否覆盖。
## 返回：保存的结果。
func SaveAs(path: String, overwrite: bool = false) -> ActionResult:
	# 校验
	if not path or path.is_empty():
		return ActionResult.new(ERR_FILE_BAD_PATH, "无效的文件名。")
	if FileAccess.file_exists(path) and not overwrite:
		return ActionResult.new(ERR_ALREADY_EXISTS, "文件“{0}”已存在。".format([ path ]))
	var count: int = GetHeadersCount()
	if count == 0:
		return ActionResult.new(ERR_INVALID_DATA, "没有文件。")
	# 如果基础目录不存在，创建基础目录
	var baseDir: String = path.get_base_dir()
	if not DirAccess.dir_exists_absolute(baseDir):
		DirAccess.make_dir_recursive_absolute(baseDir)
	# 打开源文件
	var originalFile: FileAccess = null
	if _path:
		originalFile = FileAccess.open(_path, FileAccess.READ)
	# 打开目标文件
	var newPath: String = path
	if path == _path:
		newPath = path + ".tmp"
	var destFile: FileAccess = FileAccess.open(newPath, FileAccess.WRITE)
	# 写入签名
	destFile.store_buffer(PAK_FILE_SIGN_BUFFER_XOR)
	# 写入文件头
	for idx: int in range(count):
		var header: PakFileHeader = Headers[idx]
		header.IsEndOfBlock = (idx == count -1)	#设置文件头的块结尾标识
		var headerBuffer: PackedByteArray = header.ToBuffer()
		XOR(headerBuffer)
		destFile.store_buffer(headerBuffer)
	# 写入文件内容
	for content: PakFileContent in _contents:
		var buffer: PackedByteArray = content.GetContent(originalFile)
		if not buffer or buffer.size() == 0:
			if originalFile:
				originalFile.close()
			destFile.close()
			return ActionResult.new(ERR_INVALID_DATA, "无效的文件内容：{0}。".format([ content ]))
		XOR(buffer)
		destFile.store_buffer(buffer)
	# 关闭文件
	if originalFile:
		originalFile.close()
	destFile.close()
	if _path == path:
		# 如果源文件与目标文件相同，备份源文件，并修改目标文件名
		DirAccess.rename_absolute(_path, _path + ".bak")
		DirAccess.rename_absolute(newPath, path)
	else:
		# 如果源文件与目标文件不同，修改文件路径
		_path = path
	# 生成新的文件内容数组
	var _new_contents: Array[PakFileContent] = []
	var pos: int = PAK_FILE_SIGN_LENGTH + GetHeadersLength()
	for header: PakFileHeader in Headers:
		var _new_content: PakFileContent = PakFileContent.new()
		_new_content.SetOriginal(pos, header.FileSize)
		_new_contents.append(_new_content)
		pos += header.FileSize
	_contents = _new_contents
	# 返回
	_isChanged = false
	return ActionResult.new(OK)

## 获取指定索引文件头对应的内容
## 参数：
## index-待获取内容所在的文件头索引
## 返回：返回指定索引文件头对应的内容
func GetContent(index: int) -> PackedByteArray:
	if index > GetHeadersCount() - 1:
		return []
	var pakFileContent: PakFileContent = _contents[index]
	var originalFile: FileAccess = null
	if pakFileContent.GetType() == PakFileContent.PakFileContentType.TYPE_ORIGINAL:
		originalFile = FileAccess.open(_path, FileAccess.READ)
	return pakFileContent.GetContent(originalFile)
#endregion Save

#region Unpack
## 解包特定文件
## 参数：
## index-待获取内容所在的文件头索引
## rootDir-用于存储文件的根目录；如果为空，使用当前工作目录作为根目录
## overwrite-如果文件已经存在，是否覆盖
## 返回：解包的结果
func UnpackFile(index: int, rootDir: String = "", overwrite: bool = false) -> ActionResult:
	# 检查参数
	if index > GetHeadersCount() - 1:
		return ActionResult.new(ERR_PARAMETER_RANGE_ERROR, "索引超出范围。")
	if rootDir == "":
		if OS.is_debug_build():
			rootDir = ProjectSettings.localize_path("res://")
		else:
			rootDir = OS.get_executable_path()
	var header: PakFileHeader = Headers[index]
	var path: String = rootDir + header.FileName
	if FileAccess.file_exists(path) and not overwrite:
		return ActionResult.new(ERR_ALREADY_EXISTS, "文件“{0}”已存在。".format([ path ]))
	# 获取内容
	var content: PackedByteArray = GetContent(index)
	XOR(content)
	# 如果基础目录不存在，创建基础目录
	var baseDir: String = path.get_base_dir()
	if not DirAccess.dir_exists_absolute(baseDir):
		DirAccess.make_dir_recursive_absolute(baseDir)
	# 保存文件
	var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
	file.store_buffer(content)
	file.close()
	return ActionResult.new(OK)

## 解包所有的文件
## 参数：
## rootDir-用于存储文件的根目录；如果为空，使用当前工作目录作为根目录
## overwrite-如果文件已经存在，是否覆盖
## 返回：解包的结果字典，键为文件头（PakFileHeader），值为结果（ActionResult）
func UnpackAll(rootDir: String, overwrite: bool = false) -> Dictionary:
	var dictResult: Dictionary = {}
	for idx: int in range(GetHeadersCount()):
		var fileResult: ActionResult = UnpackFile(idx, rootDir, overwrite)
		dictResult[Headers[idx]] = fileResult
	return dictResult
#endregion Unpack
	
#region Delete
## 删除所有文件
func DeleteAll() -> void:
	if GetHeadersCount() == 0:
		return
	Headers.clear()
	_contents.clear()
	_isChanged = true

## 删除特定的文件
## 参数：
## index-文件的索引
## 返回：删除的结果。
## 备注：如果索引超出范围，返回失败。
func Delete(index: int) -> ActionResult:
	if index < 0 or index >= GetHeadersCount():
		return ActionResult.new(ERR_PARAMETER_RANGE_ERROR, "文件索引超出范围。")
	Headers.remove_at(index)
	_contents.remove_at(index)
	_isChanged = true
	return ActionResult.new(OK)
#endregion Delete
	
#region Append
## 添加文件
## 参数：
## header-文件头
## content-文件内容
## 返回：添加的结果。
func Append(header: PakFileHeader, content: PakFileContent) -> ActionResult:
	if not header:
		return ActionResult.new(ERR_INVALID_DATA, "无效的文件头。")
	if not content:
		return ActionResult.new(ERR_INVALID_DATA, "无效的文件内容。")
	if Headers.any(func(h: PakFileHeader): return h.FileName == header.FileName):
		return ActionResult.new(ERR_ALREADY_EXISTS, "已存在文件。")
	Headers.append(header)
	_contents.append(content)
	_isChanged = true
	return ActionResult.new(OK)
#endregion Append

#region Insert
## 插入文件
## 参数：
## index-文件索引
## header-文件头
## content-文件内容
## 返回：添加的结果。
func Insert(index: int, header: PakFileHeader, content: PakFileContent) -> ActionResult:
	if not header:
		return ActionResult.new(ERR_INVALID_DATA, "无效的文件头。")
	if not content:
		return ActionResult.new(ERR_INVALID_DATA, "无效的文件内容。")
	var count: int = GetHeadersCount()
	if index < 0 or index >= count:
		return ActionResult.new(ERR_PARAMETER_RANGE_ERROR, "文件索引超出范围。")
	for idx: int in range(GetHeadersCount()):
		if idx != index and Headers[idx].FileName == header.FileName:
			return ActionResult.new(ERR_ALREADY_EXISTS, "已存在文件。")
	if Headers.any(func(h: PakFileHeader): return h.FileName == header.FileName):
		return ActionResult.new(ERR_ALREADY_EXISTS, "已存在文件。")
	Headers.insert(index, header)
	_contents.insert(index, content)
	_isChanged = true
	return ActionResult.new(OK)
#endregion Insert

#region Update
## 更新文件
## 参数：
## index-文件索引
## header-文件头
## content-文件内容
## 返回：更新的结果。
func Update(index: int, header: PakFileHeader, content: PakFileContent) -> ActionResult:
	if not header:
		return ActionResult.new(ERR_INVALID_DATA, "无效的文件头。")
	if not content:
		return ActionResult.new(ERR_INVALID_DATA, "无效的文件内容。")
	var count: int = GetHeadersCount()
	if index < 0 or index >= count:
		return ActionResult.new(ERR_PARAMETER_RANGE_ERROR, "文件索引超出范围。")
	for idx: int in range(GetHeadersCount()):
		if idx != index and Headers[idx].FileName == header.FileName:
			return ActionResult.new(ERR_ALREADY_EXISTS, "已存在文件。")
	Headers[index] = header
	_contents[index] = content
	_isChanged = true
	return ActionResult.new(OK)
#endregion Update
